package moltencore.game;

import moltencore.game.bubble.Bubble;
import moltencore.game.bubble.Type;
import moltencore.game.bubble.BubbleRandomizer;
import moltencore.game.collision.ICollidable;
import moltencore.config.Config;

import java.util.*;

import spark.math.*;
import spark.utility.OutBoolean;
import spark.utility.IncrementalShortIdFactory;
import org.apache.log4j.Logger;

public class Field extends AbstractField<Bubble, Player> {
	private static Logger logger = Logger.getLogger(Field.class);

	private LinkedList<IGameEventListener> gameEventListeners = new LinkedList<IGameEventListener>();

	private IncrementalShortIdFactory bubbleIdFactory = new IncrementalShortIdFactory();
	private BubbleRandomizer bubbleTypeRandomizer;

	private final int pushCount;
	private int pushCounter = 0;

	private int criticalMass;
	private ScoreCalculator scoreCalculator = new ScoreCalculator();

	private Grid attachedBubbles;
	private LinkedList<Bubble> movingBubbles = new LinkedList<Bubble>();
	/**
	 * Pattern bubbles that contain grid coordinates for pushing down a new row
	 */
	private LinkedList<Bubble> pushBubbles;

	private ArrayList<ICollidable> restrictedAreas;
	private boolean hasBubbleInRestrictedArea = false;

	private PushType pushType;

	/**
	 * @param fieldId          Unique id (within a game)
	 * @param fieldFile        File used to initialize this field (levels folder)
	 * @param criticalMass     How many bubbles need to form a chain until they pop
	 * @param windEnabled      Should the field have wind that influences bubbles?
	 * @param gravityEnabled   Should the field have gravity that influences bubbles?
	 * @param attachedBubbles  Grid containing bubbles initially attached to the grid
	 * @param pushBubbles      Reference bubble positions which indicate where new bubbles will be spawned when a row
	 *                         of bubbles is pushed.
	 * @param bubbleRandomizer Randomizer to generate new bubbles (a copy of this is created)
	 * @param collidables      Walls etc. all moving bubbles can collide against
	 * @param restrictedAreas  Collidables no attached bubble may enter
	 * @param criticalAreas    Collidables that make a moving bubble into a penalty bubble if entered
	 * @param cannonData       Information about cannon properties specified in the level file
	 * @param pushCount        Determines after how many shots the ceiling will be lowered
	 * @param pushType         Determines the event that happens after pushCount bubbles have been fired
	 */
	public Field(long fieldId, String fieldFile,
	             int criticalMass, boolean windEnabled, boolean gravityEnabled,
	             Grid attachedBubbles, LinkedList<Bubble> pushBubbles, BubbleRandomizer bubbleRandomizer,
	             ArrayList<ICollidable> collidables,
	             ArrayList<ICollidable> restrictedAreas,
	             ArrayList<ICollidable> criticalAreas,
	             Collection<CannonData> cannonData, int pushCount,
	             PushType pushType) {
		super(fieldId, fieldFile, windEnabled, gravityEnabled, collidables, criticalAreas, cannonData);
		this.pushBubbles = pushBubbles;
		this.attachedBubbles = attachedBubbles;
		this.criticalMass = criticalMass;
		this.bubbleTypeRandomizer = bubbleRandomizer;
		this.pushCount = pushCount;
		this.restrictedAreas = restrictedAreas;
		this.pushType = pushType;
		configurePushBubbles();
	}

	private void configurePushBubbles() {
		if(!pushBubbles.isEmpty()) {
			int rightMostPushBubbleIndex = 0;
			Bubble rightMostPushBubble = pushBubbles.get(rightMostPushBubbleIndex);
			for(int i = 0; i < pushBubbles.size(); i++) {
				Bubble current = pushBubbles.get(i);
				if(current.getGridPosition().x > rightMostPushBubble.getGridPosition().x) {
					rightMostPushBubble = current;
					rightMostPushBubbleIndex = i;
				}
			}
			//remove the right most push bubble from the list and add it at the end of the list
			//we do this because the right most push bubble is removed if the row it is in is indented
			pushBubbles.remove(rightMostPushBubbleIndex);
			pushBubbles.add(rightMostPushBubble);
		}
	}

	public void pushRow() {
		if(pushBubbles.isEmpty()) {
			return;
		}
		LinkedList<Bubble> pushRow = new LinkedList<Bubble>();
		for(Bubble patternBubble : pushBubbles) {
			Bubble pushBubble = new Bubble(bubbleTypeRandomizer.randomBubbleType(), bubbleIdFactory.getNextId());
			pushBubble.getGridPosition().x = patternBubble.getGridPosition().x;
			pushBubble.getGridPosition().y = patternBubble.getGridPosition().y;
			pushRow.add(pushBubble);
		}
		//remove the last push bubble (the right most) if the row it is in will be indented
		if(!attachedBubbles.isRowIndented(pushRow.getLast().getGridPosition().y)) {
			pushRow.removeLast();
		}
		Vector2 pushDirection = attachedBubbles.pushRow(pushRow);
		notifyListenersRowPushed(pushRow, pushDirection);
		// after the push check if an attached bubble has ended up in the restricted area
		if(isGridBubbleInRestrictedArea()) {
			hasBubbleInRestrictedArea = true;
		}
	}

	public void pushCeiling() {
	}

	public Bubble createRandomBubble() {
		return new Bubble(bubbleTypeRandomizer.randomBubbleType(), bubbleIdFactory.getNextId());
	}

	/**
	 * Spawns a moving bubble on the game field
	 *
	 * @param bubble Bubble to add
	 */
	public void spawnMovingBubble(Bubble bubble) {
		movingBubbles.add(bubble);
		notifyListenersBubbleFired(bubble);
	}

	/**
	 * Shoots a penalty bubble from the bottom of the field at a random position
	 *
	 * @param bubble Bubble to be spawned as penalty bubble
	 * @return Initialized penalty bubble
	 */
	public Bubble initializePenaltyBubble(Bubble bubble) {
		// give the bubble a new id because the client still has the old id in use
		bubble.setBubbleId(bubbleIdFactory.getNextId());
		bubble.setPushCounted(false);
		int position = spark.math.Random.getInstance().nextInt(pushBubbles.size());
		//todo - a line across which penalty bubbles are spawned should be specified in the level files
		bubble.getPosition().y = cannonData.values().iterator().next().getPosition().y;
		bubble.getPosition().x = pushBubbles.get(position).getPosition().x;
		//todo - this should probably be configured somewhere
		bubble.getVelocity().y = 20;
		return bubble;
	}

	private void handleMovingBubbleFinalization(Bubble bubble) {
		if(pushType != PushType.NeverPush && bubble.isPushCounted()) {
			pushCounter++;
			if(pushCounter == pushCount) {
				pushCounter = 0;
				switch(pushType) {
					case Row:
						pushRow();
						break;
					case Ceiling:
						pushCeiling();
						break;
					default:
						assert false;
				}
			}
		}
	}

	@Override
	public void update(float elapsedTimeSeconds) {
		OutBoolean attachBubble = new OutBoolean();
		LinkedList<Bubble> penaltyBubbles = null;
		for(Iterator<Bubble> i = movingBubbles.iterator(); i.hasNext();) {
			Bubble bubble = i.next();
			applyGravity(bubble, gravity, elapsedTimeSeconds);
			Vector2 movement = calculateMovement(bubble, elapsedTimeSeconds);
			attachBubble.value = false;
			for(ICollidable collidable : attachedBubbles.getBubbles()) {
				boolean collisionOccurred = handleMovingBubbleCollisionLogic(collidable, bubble, movement, attachBubble);
				if(collisionOccurred) {
					logger.debug("Col with bubble at position " + ((Bubble) collidable).getPosition() + " at grid " + ((Bubble) collidable).getGridPosition());
					break;
				}
			}
			handleCollidablesCollision(bubble, movement, attachBubble);
			//check collision against field's critical areas
			for(ICollidable criticalArea : criticalAreas) {
				if(criticalArea.collides(bubble, movement)) {
					// if the bubble entered a critical area we remove it from the moving bubble and pop it
					i.remove();
					notifyListenersBubblePopped(bubble);
					handleMovingBubbleFinalization(bubble);
					// as a penalty we initialize it to become a penalty bubble (re-use the instance)
					if(penaltyBubbles == null) {
						penaltyBubbles = new LinkedList<Bubble>();
					}
					penaltyBubbles.add(initializePenaltyBubble(bubble));
					break;
				}
			}
			if(attachBubble.value) {
				i.remove();
				attach(bubble);
				handleMovingBubbleFinalization(bubble);
				//if after attaching the bubble it landed in the restricted area we stop updating
				if(hasBubbleInRestrictedArea) {
					return;
				}
				continue;
			}
			applyMovement(bubble, movement);
		}
		if(penaltyBubbles != null) {
			for(Bubble penaltyBubble : penaltyBubbles) {
				spawnMovingBubble(penaltyBubble);
			}
		}
	}

	protected boolean handleMovingBubbleCollisionLogic(ICollidable collidable, Bubble bubble, Vector2 movement, OutBoolean attachBubble) {
		boolean collided = super.handleMovingBubbleCollisionLogic(collidable, bubble, movement, attachBubble);

		if(collided && attachBubble.value) {
			bubble.setAttachedToWorld(collidable.isPartOfWorld());
		}

		return collided;
	}

	private void attach(Bubble bubble) {
		logger.debug("Snapping bubble with position " + bubble.getPosition());
		attachedBubbles.snapToGrid(bubble);
		boolean restricted = isBubbleInRestrictedArea(bubble);
		notifyListenersBubbleFixated(bubble);

		if(bubble.getType().isOrdinary() || bubble.getType() == Type.Rainbow) {
			LinkedList<Bubble> bubbleChain = bubble.getBubbleChain().getChainBubblesByCriticalMass(criticalMass);
			if(!bubbleChain.isEmpty())
				dissolveBubbles(bubbleChain/*, false*/);
		}
		else if(bubble.getType() == Type.Blazing) {
			Vector<Bubble> toDestroy = new Vector<Bubble>();
			for(Bubble neighbor : bubble.getNeighbors()) {
				if(neighbor != null) {
					toDestroy.add(neighbor);
				}
			}
			toDestroy.add(bubble);
			dissolveBubbles(toDestroy/*, true*/);
		}
		//if the bubble was in the critical area and was not popped as part of a chain etc. we have a bubble in the
		//critical area
		if(restricted && !bubble.isHangingFree()) {
			hasBubbleInRestrictedArea = true;
			logger.debug("Bubble in restricted area detected on field " + getFieldId());
		}
	}

	private boolean isBubbleInRestrictedArea(Bubble bubble) {
		for(ICollidable restrictedArea : restrictedAreas) {
			if(restrictedArea.collides(bubble, null)) {
				return true;
			}
		}
		return false;
	}

	private boolean isGridBubbleInRestrictedArea() {
		// we only have horizontal restricted areas in the game so it suffices to check the lowest bubbles of the
		// grid against the restricted areas
		Bubble lowest = null;
		for(Bubble bubble : attachedBubbles.getBubbles()) {
			if(lowest != null) {
				if(bubble.getPosition().y < lowest.getPosition().y) {
					lowest = bubble;
				}
			}
			else {
				lowest = bubble;
			}
		}
		return isBubbleInRestrictedArea(lowest);
	}

	private void dissolveBubbles(Collection<Bubble> bubbles/*, boolean dissolveSurroundingGhostBubbles*/) {
		Vector<Bubble> chain = new Vector<Bubble>(bubbles);
		Vector<Bubble> surroundingBubbles = Bubble.getSurroundingBubbles(bubbles);
		scoreCalculator.startNewBubbleDrop();

		scoreCalculator.setChainBubbles(chain.size());
		for(Bubble currentBubble : chain) {
			pop(currentBubble);
		}

		for(Bubble currentBubble : surroundingBubbles) {
			if(currentBubble.isHangingFree()) {
				scoreCalculator.addBranchBubbles((popBubbleMass(currentBubble)));
				continue;
			}
//			if(dissolveSurroundingGhostBubbles && currentBubble.getType() == Type.Ghost) {
//				Vector<Bubble> bubblesAroundGhostBubble = new Vector<Bubble>();
//	            for(Bubble neighbor : currentBubble.getNeighbors()) {
//		            if(neighbor != null) {
//			            bubblesAroundGhostBubble.add(neighbor);
//		            }
//	            }
//				pop(currentBubble);
//				for(Bubble currentBubbleAroundGhostBubble : bubblesAroundGhostBubble) {
//					if(currentBubbleAroundGhostBubble.isHangingFree()) {
//						popBubbleMass(currentBubbleAroundGhostBubble);
//					}
//				}
//			}
		}
		score += scoreCalculator.calculateScore();
		notifyListenersScoreUpdated();
	}

	public void pop(Bubble bubble) {
		attachedBubbles.clearBubble(bubble);
		bubble.detach();
		notifyListenersBubblePopped(bubble);
	}

	private int popBubbleMass(Bubble bubble) {
		int bubbleCount = bubble.gatherBubbleMass().size();
		for(Bubble currentBubble : bubble.gatherBubbleMass()) {
			pop(currentBubble);
		}
		return bubbleCount;
	}

	public boolean isBusted() {
		return hasBubbleInRestrictedArea;
	}

	public int getGridBubbleCount() {
		return attachedBubbles.getBubbles().size();
	}

	public int getMovingBubbleCount() {
		return movingBubbles.size();
	}

	public void addGameEventListener(IGameEventListener listener) {
		if(gameEventListeners.contains(listener)) {
			return;
		}
		gameEventListeners.add(listener);
	}

	public void removeGameEventListener(IGameEventListener listener) {
		gameEventListeners.remove(listener);
	}

	private void notifyListenersBubbleFixated(Bubble bubble) {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.bubbleFixated(bubble, this.getFieldId());
		}
	}

	private void notifyListenersScoreUpdated() {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.newScore(this.getFieldId(), (int) score);
		}
	}

	private void notifyListenersBubblePopped(Bubble bubble) {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.bubblePopped(bubble, this.fieldId);
		}
	}

	protected void notifyListenersRowPushed(LinkedList<Bubble> row, Vector2 direction) {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.rowPushed(getFieldId(), row, direction);
		}
	}

	protected void notifyListenersCeiling(Vector2 direction) {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.ceilingPushed(getFieldId(), direction);
		}
	}

	private void notifyListenersBubbleFired(Bubble bubble) {
		for(IGameEventListener gameEventListener : gameEventListeners) {
			gameEventListener.bubbleFired(bubble, this.fieldId);
		}
	}

	public Grid getAttachedBubbles() {
		return attachedBubbles;
	}

	public static enum PushType {
		Row,
		Ceiling,
		NeverPush
	}
}
